package com.example.arffrecoder.services;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

import com.example.arffrecorder.R;
import com.example.arffrecorder.graphics.RecordingActivity;
import com.example.arffrecorder.graphics.UserAction;
import com.example.arffrecorder.utils.ARFFFileWriter;
import com.example.arffrecorder.utils.MovementData;

import weka.classifiers.Classifier;
import weka.classifiers.bayes.NaiveBayes;
import weka.classifiers.lazy.IBk;
import weka.classifiers.trees.J48;
import weka.core.Instance;
import weka.core.Instances;
import actclient.ClassLabel;
import actclient.CoordinatorClient;
import actclient.RoomState;
import actclient.CoordinatorClient.UserState;
import actclient.GroupStateListener;
import actclient.UserRole;
import android.app.IntentService;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Intent;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Binder;
import android.os.Environment;
import android.os.IBinder;
import android.text.method.DateTimeKeyListener;
import android.widget.Toast;

public class ARFFLocationService extends IntentService implements
		SensorEventListener {

	// CoordinatorClient object for our user;
	private CoordinatorClient client = new CoordinatorClient("1356427");
	// index needed for trimming the array with data from different window sizes
	private int window_no = 0;
	// index of the first data entry for a window size
	ArrayList<Integer> indices = new ArrayList<Integer>();
	// parameters for window segmentation
	private int window_size = 300;
	// the size of jump when classifying
	private int jump = 100;
	// Create training data instance
	Instances trainingData = null;
	// The classifier used 
	Classifier classifier;
	String activity_type = "Idle";
	// arraylist containing the raw data recorded by the service
	ArrayList<MovementData> recordedData = new ArrayList<MovementData>();
	ArrayList<MovementData> computeData = new ArrayList<MovementData>();
	// timer for getting the index of the first data entry in a window size
	Timer indexTimer = new Timer();
	// timer for calling the computing function of the features in a window
	// size;
	Timer computeTimer = new Timer();
	public static final String BROADCAST_ACTION = "com.example.arffrecorder.displayevent";
	Intent intent;
	Intent visualizeIntent;
	// Keep all user states in one minute
	ArrayList<UserAction> userActionsList = new ArrayList<UserAction>();
	//Keep track of offline users
	ArrayList<String>offline = new ArrayList<String>();
	// Time when service starts, used to calculate user roles every interval
	long startupTime;
	GroupStateListener groupStateListener;
	GroupStateListener destroyGroup;
	// State of the room: lecture, transition or empty
	private RoomState currentRoomState=RoomState.empty;
	
	public ARFFLocationService() {
		super("Recording Service");
		
	}

	private NotificationManager myNM;

	// the id corresponding to the string that will be displayed by the
	// notification on the start of the service
	private int NOTIFICATION = R.string.record_service_started;

	// sensors needed for recording data from the accelerometer
	private SensorManager mSensorManager;
	private Sensor mAccelerometer;

	private float sensorX, sensorY, sensorZ;
	
	private boolean firstRun;
	/**
	 * Class for clients to access. Because we know this service always runs in
	 * the same process as its clients, we don't need to deal with IPC.
	 */

	public class LocalBinder extends Binder {
		public ARFFLocationService getService() {
			return ARFFLocationService.this;
		}
	}

	@Override
	public void onCreate() {
		super.onCreate();
		//initialize the arraylist of users
		offline = new ArrayList<String>();
		userActionsList = new ArrayList<UserAction>();
		intent = new Intent(BROADCAST_ACTION);
		// initialize the notification manager for our service
		myNM = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

		// initialize the sensors for getting data from accelerometer
		mSensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
		mAccelerometer = mSensorManager
				.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
		// register listener for the accelerometer at the highest rate
		mSensorManager.registerListener(this, mAccelerometer,
				SensorManager.SENSOR_DELAY_FASTEST);
		client.removeGroupStateListener(destroyGroup);
		firstRun = true;
		// Display a notification about starting the service. We put an icon in
		// the status bar.
		showNotification();
		createClassifier();
		// first start collecting data and get the index of the first element
		// for every window
		indexTimer.scheduleAtFixedRate(new TimerTask() {
			@Override
			public void run() {
				getIndexOfFirstRawData();
			}
		}, 0, jump);
		/**
		 * after every window size perform the computations for the feature
		 * vector. First computation starts after the time frame of a window
		 **/
		computeTimer.scheduleAtFixedRate(new TimerTask() {
			@Override
			public void run() {
				computeFeatures();
			}
		}, window_size, jump);
	}

	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		super.onStartCommand(intent, flags, startId);
		String fileName = intent.getStringExtra("fileName");
		ARFFFileWriter.getInstance().createFile(fileName);
		
		// Used to measure intervals to calculate user roles
		startupTime = System.currentTimeMillis();
		
		//Registering with the server
		groupStateListener  = new GroupStateListener() {

			/**
			 * Method called everytime the server sends new data to calculate
			 * the user roles
			 */
			@Override
			public void groupStateChanged(UserState[] groupState) {
				//creating the window size and jump for the setting users' roles and room's state
				long currentTime = System.currentTimeMillis();
				long timeDifference = currentTime - startupTime;
				long interval = 1 * 60 * 1000;
				int seqTime = (int) (timeDifference % interval) / 1000;
				
				/** If this is the first run wait one minute to calculate roles and set states,
				 *  otherwise only wait 5 seconds**/
				if ((seqTime >= 59 && firstRun) || (seqTime >=5 && !firstRun)) {
					if(firstRun){
						firstRun = false;
					}
					calculateUserRoles();
					
					int numSpeakers = 0, numListeners = 0, numTransition = 0;
					/**increment the number of specific users in a group according to their roles
					 * userActionList has all users and lists for their activites and corresponding update times**/
					for(UserState ust : groupState){
						for(UserAction uac : userActionsList){
							if(ust.getUserId().equals(uac.userId)){
								ust.setRole(uac.role);
								if(uac.role.equals(UserRole.speaker)){
									numSpeakers++;
								}
								if(uac.role.equals(UserRole.listener)){
									numListeners++;
								}
								if(uac.role.equals(UserRole.transition)){
									numTransition++;
								}
							}
						}
					}
					/**deciding room's state after knowing the users' roles**/
					if(numSpeakers == 0 && numListeners == 0 && numTransition == 0){
						currentRoomState = RoomState.empty;
						client.setRoomState(RoomState.empty);
					}
					else{
						if(numSpeakers == 1 && numListeners > 0 && numTransition == 0){
							currentRoomState = RoomState.lecture;
							client.setRoomState(RoomState.lecture);
						}
						else{
							currentRoomState = RoomState.transition;
							client.setRoomState(RoomState.transition);
						}
					}
					sendVisualizeData(currentRoomState, userActionsList, offline);

					/** Clear the userActionList and rest startup time for new window**/
					userActionsList = new ArrayList<UserAction>();
					//offline = new ArrayList<String>();
					startupTime = System.currentTimeMillis();
				}

				/** Populate an arraylist of all useractions. For each unique
				 * user have an entry with his activities and their times
				 */

				for (UserState us : groupState) {
					//userStates.add(us);
					Boolean foundEntry = false;
					
					for (int i = 0; i < userActionsList.size(); i++) {
						if (us.getUserId().equals(userActionsList.get(i).userId)) {
							foundEntry = true;
							/**If a user is offline add him to an offline list and set role to null
							 * and remove him from the userActionList**/
							if(us.getUpdateAge() > 10000){
								if(!offline.contains(userActionsList.get(i).userId)){
									offline.add(userActionsList.get(i).userId);
									us.setRole(null);
								}
								userActionsList.remove(i);
								continue;
							}
							userActionsList.get(i).activities.add(us.getActivity());
							userActionsList.get(i).updateTimes.add(us.getUpdateAge());
						}
					}
					/** Adding a new user to the userActionList **/
					if (!foundEntry) {
						if(us.getUpdateAge() > 10000 || offline.contains(us.getUserId())){
							us.setRole(null);
							continue;
						}
						UserAction newUserAction = new UserAction();
						newUserAction.userId = us.getUserId();
						newUserAction.activities.add(us.getActivity());
						newUserAction.updateTimes.add(us.getUpdateAge());
						userActionsList.add(newUserAction);
					}

					// us.setRole(r < 0.25 ? UserRole.transition : r < 0.5 ?
					// UserRole.listener : r < 0.75 ? UserRole.speaker : null);
				}
				

				/*starting the visualization activity
				Intent dialogIntent = new Intent(null, RecordingActivity.class);
				dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
				dialogIntent.putExtra("room_state", currentRoomState);
				startActivity(dialogIntent);*/
			}
			
			/**
			 * This method uses a list of user actions. one entry for each unique user with his
			 * activities and updates and sets his role
			 */
			private void calculateUserRoles() {
				double interval = 1.0*60.0*1000.0;
				/**For each user find how many activities of each type and their duration in the 
				 * last 5 seconds and find the most likely activity
				 */
				for(UserAction userAction : userActionsList){
					double timeWalking = 0;
					double timeSitting = 0;
					double timeStanding = 0;
					String role;
					for(int i = 0; i < userAction.updateTimes.size(); i++){
						ClassLabel activity = userAction.activities.get(i);
						long activityTime = 0; 
						
						/** Set offline users to transition **/
						if(activity == null || userAction.updateTimes.get(i)>=10000){
							userAction.role = UserRole.transition;
							continue;
						}
						else{
							if(activity.equals(ClassLabel.walking)){
							activityTime = userAction.updateTimes.get(i);
							timeWalking += activityTime*1.0;
						}
						if(activity.equals(ClassLabel.standing)){
							activityTime = userAction.updateTimes.get(i);
							timeStanding += activityTime*1.0;
						}
						if(activity.equals(ClassLabel.sitting)){
							activityTime = userAction.updateTimes.get(i);
							timeSitting += activityTime*1.0;
						}
						}
					}
					/** Calculate percentage of time spend in each activity mode **/
					double scoreWalking = timeWalking/(timeWalking+timeStanding+timeSitting);
					double scoreStanding = timeStanding/(timeWalking+timeStanding+timeSitting);
					double scoreSitting = timeSitting/(timeWalking+timeStanding+timeSitting);
					
						if((scoreStanding + scoreWalking > 0.75) && (scoreStanding - scoreWalking > 0.2)){
							userAction.role = UserRole.speaker;
						}
						else{
							if(scoreSitting > 0.75){
								userAction.role = UserRole.listener;
							}
							else{
								userAction.role = UserRole.transition;
							}
						}
					
					
				}
				
				
			}

			private void calculateRolesHelper(
					ArrayList<UserAction> userActionsList) {
				// TODO Auto-generated method stub

			}
		};
		client.addGroupStateListener(groupStateListener);
		
		return START_STICKY;
	}

	@Override
	public void onDestroy() {
		// Cancel the persistent notification.
		myNM.cancel(NOTIFICATION);
		indexTimer.cancel();
		indexTimer.purge();
		computeTimer.cancel();
		computeTimer.purge();
		// Tell the user we stopped.
		Toast.makeText(this, R.string.record_service_stopped,
				Toast.LENGTH_SHORT).show();

		// unregister the sensor from recording data from the accelerometer
		mSensorManager.unregisterListener(this);
		// ARFFFileWriter.getInstance().finishWriting();*/
		//remove user roles assigned to the users
		destroyGroup = new GroupStateListener() {
		
			@Override
			public void groupStateChanged(UserState[] groupState) {
				for (UserState us: groupState)
					us.setRole(null);
				
			}
		};
		client.addGroupStateListener(destroyGroup);
		client.removeGroupStateListener(groupStateListener);
		super.onDestroy();
	}

	@Override
	public IBinder onBind(Intent intent) {
		return null;
	}

	/**
	 * display a notification while the service is running
	 * 
	 */
	private void showNotification() {
		// message to be displayed
		CharSequence text = getText(R.string.record_service_started);

		// Set the icon, scrolling text and timestamp
		Notification notification = new Notification(
				R.drawable.notification_icon, text, System.currentTimeMillis());

		// The PendingIntent to launch our activity if the user selects this
		// notification
		PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
				new Intent(this, RecordingActivity.class), 0);

		// Set the info for the views that show in the notification panel.
		notification.setLatestEventInfo(this,
				getText(R.string.record_service_label), text, contentIntent);

		// Send the notification.
		myNM.notify(NOTIFICATION, notification);
	}

	@Override
	public void onAccuracyChanged(Sensor sensor, int accuracy) {
		// TODO Auto-generated method stub
	}

	@Override
	/**
	 * notify any changes on the accelerometer data, write it in the file and display it in the activity
	 */
	public void onSensorChanged(SensorEvent event) {

		sensorX = (float) (((int) (event.values[0] * 10)) / 10.0);
		sensorY = (float) (((int) (event.values[1] * 10)) / 10.0);
		sensorZ = (float) (((int) (event.values[2] * 10)) / 10.0);
		recordedData.add(new MovementData(event.values[0], event.values[1],
				event.values[2], System.currentTimeMillis()));

		sendData(sensorX, sensorY, sensorZ);
		// ARFFFileWriter.getInstance().WriteTofile(sensorX, sensorY, sensorZ);
	}

	@Override
	protected void onHandleIntent(Intent intent) {
	}

	/**
	 * send data to the activity via a broadcast message
	 */
	public void sendData(float sensorX, float sensorY, float sensorZ) {
		intent.putExtra("message_type", "raw_data");
		intent.putExtra("ox", sensorX + "");
		intent.putExtra("oy", sensorY + "");
		intent.putExtra("oz", sensorZ + "");
		sendBroadcast(intent);
	}

	public void sendActivity(String activity_type) {
		intent.putExtra("message_type", "display_label");
		intent.putExtra("activity_value", activity_type);
		sendBroadcast(intent);

	}
	public void sendVisualizeData(RoomState rs, ArrayList<UserAction> userActionsList, ArrayList<String>offline){
		intent.putExtra("message_type", "show_room");
		intent.putExtra("room_state", rs.toString());
		intent.putStringArrayListExtra("offline_users", offline);
		ArrayList<String> onlineIds = new ArrayList<String>();
		ArrayList<String> onlineRoles = new ArrayList<String>();
		if (firstRun){
			
			for (int i=0;i<userActionsList.size();i++){
				onlineIds.add(userActionsList.get(i).getUserId());
				onlineRoles.add("transition");
			}
			
		}
		else{
			for (int i=0;i<userActionsList.size();i++){
				onlineIds.add(userActionsList.get(i).getUserId());
				onlineRoles.add(userActionsList.get(i).getRole().toString());
			}
		}
		intent.putStringArrayListExtra("onlineIds", onlineIds);
		intent.putStringArrayListExtra("onlineRoles", onlineRoles);
		sendBroadcast(intent);
		
	}

	/**
	 * get the index of the first element for the arraylist of raw data that has
	 * to be included in the computation of the feature for the current window
	 */
	public void getIndexOfFirstRawData() {
		indices.add(recordedData.size() - 1);
		window_no++;
	}

	/**
	 * make the computations of the features from the raw data measured from the
	 * accelerometer (in our case the mean)
	 * 
	 */
	public void computeFeatures() {
		int start = 0;
		if (indices.get(window_no - 2) == -1)
			start = 0;
		else
			start = indices.get(window_no - 2);
		int end = 0;
		if (indices.get(window_no - 1) == -1)
			end = 0;
		else
			end = indices.get(window_no - 1);
		// we need the raw data contained between two window jumps
		for (int i = start; i < end; i++)
			computeData.add(recordedData.get(i));
		float mean_x = 0;
		float mean_y = 0;
		float mean_z = 0;
		for (MovementData md : computeData) {
			mean_x += md.getX();
			mean_y += md.getY();
			mean_z += md.getZ();
		}
		mean_x /= computeData.size();
		mean_y /= computeData.size();
		mean_z /= computeData.size();
		computeData.clear();
		classifyActivity(mean_x, mean_y, mean_z);

	}

	/**
	 * initialize and set the options for the classifier
	 */
	public void createClassifier() {
		File root = Environment.getExternalStorageDirectory();
		File infile = new File(root, "training_data/trainingData.arff");

		try {
			BufferedReader br = new BufferedReader(new FileReader(infile));
			trainingData = new Instances(br);
			br.close();
		} catch (IOException e) {
			e.printStackTrace();
		}

		if (trainingData.classIndex() == -1) {
			trainingData.setClassIndex(trainingData.numAttributes() - 1);
		}
		// String options[] = new String[] {"-K 1 -W 0"};
		// Create the classifier for the training data

		/*
		 * classifier = new J48(); String options[] = new
		 * String[]{"-C 0.5 -M 2"}; try { classifier.setOptions(options);
		 * classifier.buildClassifier(trainingData); } catch (Exception e1) { //
		 * TODO Auto-generated catch block e1.printStackTrace(); }
		 */

		classifier = new IBk();
		// String options[] = new String[]{"-K 1 -W 0 -A"};
		try {
			// classifier.setOptions(options);
			classifier.buildClassifier(trainingData);
		} catch (Exception e) {
			e.printStackTrace();
		}

		/*
		 * classifier = new NaiveBayes(); try {
		 * classifier.buildClassifier(trainingData); } catch (Exception e) { //
		 * TODO Auto-generated catch block e.printStackTrace(); }
		 */
	}

	/**
	 * pass the feature data vector to the classifier and return the activity
	 * type established by the classifier. send the result to the activity in
	 * order to display it.
	 * 
	 * @param mean_x
	 * @param mean_y
	 * @param mean_z
	 */
	public void classifyActivity(float mean_x, float mean_y, float mean_z) {
		Instance di = new Instance(4);
		di.setValue(0, mean_x);
		di.setValue(1, mean_y);
		di.setValue(2, mean_z);
		di.setDataset(trainingData);
		double clLabel;
		try {
			clLabel = classifier.classifyInstance(di);
			di.setClassValue(clLabel);
			activity_type = di.classAttribute().value((int) clLabel);
			client.setCurrentActivity(ClassLabel.valueOf(activity_type));
			sendActivity(activity_type);

		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}